/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package uk.ac.rdg.resc.mmd.colocation;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import org.joda.time.DateTime;
import uk.ac.rdg.resc.mmd.ncwms.config.LayerImpl;
import uk.ac.rdg.resc.mmd.ncwms.config.datareader.DataReader;
import uk.ac.rdg.resc.mmd.ncwms.config.datareader.DefaultDataReader;
import uk.ac.rdg.resc.mmd.ncwms.coords.LonLatPosition;
import uk.ac.rdg.resc.mmd.ncwms.coords.LonLatPositionImpl;
import uk.ac.rdg.resc.mmd.ncwms.exceptions.InvalidDimensionValueException;

/**
 *
 * @author Alastair
 */
public class NcModelTimeseries {

    private ObsProxyData obsProxyData;
    private List<Double> allModelElevations;
    private double elevation;
    private LonLatPosition obsPosition;
    private LonLatPosition modelPosition;
    private List<DateTime> dates;
    
    private Map<String, LayerImpl> layers;
    private LayerImpl layer;

    

    public NcModelTimeseries(ObsProxyData obsProxyData, String modelLocation) throws FileNotFoundException, IOException, InvalidDimensionValueException {
        this.obsProxyData = obsProxyData;

        // Get an object to read data from NetCDF files
        DataReader dr = new DefaultDataReader();

        // Load up all the metadata from a collection of NetCDF files, which are
        // automatically aggregated along their time axis
        this.layers = dr.getAllLayers(modelLocation);

        // Print out all the layer IDs (internal variable IDs)
        for (String layerId : this.layers.keySet()) {
            System.out.println(layerId);
        }

        // Need to change this layer name hardcoding - give user choice of layer
        // names to choose from a jsp page.
        String defaultLayerName = "temp";
        this.layer = this.layers.get(defaultLayerName);
        this.layer.setDataReader(dr);

        // Set the List of all model Elevations. Note that some of these may be
        // under the sea floor for this particular lon/lat point. However, only
        // the valid ones are returned by the getElevations() method.
        this.allModelElevations = layer.getElevationValues();
        this.elevation = layer.getDefaultElevationValue();

        // Set the obs position, and use it to get the nearest model position (so that
        // clients can ask for the model position which was used for the timeseries).
        this.obsPosition = new LonLatPositionImpl(obsProxyData.getLon(), obsProxyData.getLat());
        int[] gridCoords = layer.getHorizontalCoordSys().lonLatToGrid(this.obsPosition);
        this.modelPosition = layer.getHorizontalCoordSys().gridToLonLat(gridCoords);

        // Use the dates from the obs RosData to set the model timeseries dates
        this.dates = getDatesFromRosData();
    }

    public Set<String> getParameters() {
        return this.layers.keySet();
    }

    public List<DateTime> getDates() {
        return this.layer.getTimeValues();
    }

    public List<Double> getAllElevations() {
        return this.allModelElevations;
    }

    public List<Double> getNonNullElevations() throws InvalidDimensionValueException, IOException {
        return getValidElevationsAtThisPosition();
    }

    public void setElevation(double elev) {
        this.elevation = elev;
    }

    public double getElevation() {
        return this.elevation;
    }

    public String getElevationUnits() {
        return this.layer.getElevationUnits();
    }

    public String getParameterName() {
        //return this.layer.getId();
        return this.layer.getTitle();
    }
    
    public String getParameterUnits() {
        return this.layer.getUnits();
    }

    public LonLatPosition getPosition() {
        return this.modelPosition;
    }

    public List<Float> getValues() throws InvalidDimensionValueException, IOException {
        return this.layer.readTimeseries(this.dates, this.elevation, this.obsPosition);
    }

    public List<List<Float>> getProfileTimeseriesValues() throws InvalidDimensionValueException, IOException {
        return this.layer.readProfileTimeseries(dates, allModelElevations, obsPosition);
    }

    public SortedMap<DateTime, Float> getData () throws InvalidDimensionValueException, IOException {
        List<Float> values = layer.readTimeseries(this.dates, this.elevation, this.obsPosition);

        SortedMap<DateTime, Float> data = new TreeMap<DateTime, Float>();
        for (int i = 0; i < this.dates.size(); i++) {
            data.put(this.dates.get(i), values.get(i));
        }
        return data;
    }

    private List<DateTime> getDatesFromRosData() {
        List<DateTime> rosDates = this.obsProxyData.getDates();
        List<DateTime> allModelDates = this.layer.getTimeValues();

        DateTime firstRosDate = rosDates.get(0);
        DateTime lastRosDate = rosDates.get(0);
        for (DateTime rosDate : rosDates) {
            if (rosDate.isAfter(lastRosDate)) {
                lastRosDate = rosDate;
            }
            if (rosDate.isBefore(firstRosDate)) {
                firstRosDate = rosDate;
            }
        }
        System.out.println("firstRosDate: " + firstRosDate);
        System.out.println("lastRosDate: " + lastRosDate);

        int firstModelIndex = allModelDates.size();
        int lastModelIndex = allModelDates.size();
        int currentIndex = 0;
        for (DateTime modelDate : allModelDates) {
            System.out.println("modelDate: " + modelDate);
            if (!modelDate.isBefore(firstRosDate) && firstModelIndex == allModelDates.size()) {
                firstModelIndex = currentIndex;
            }
            if (modelDate.isAfter(lastRosDate) && lastModelIndex == allModelDates.size()) {
                lastModelIndex = currentIndex;
            }
            currentIndex++;
        }
        System.out.println("firstModelIndex: " + firstModelIndex);
        System.out.println("lastModelIndex: "  + lastModelIndex);

        return allModelDates.subList(firstModelIndex, lastModelIndex);
    }

    // Returns a list of Elevations that have data AT THE LAT/LON POSITION OF
    // THIS TIMESERIES. We do this by trying to read data at a given depth
    // and checking if we get a null value (below the ocean floor) or if we
    // get a float (in the water column). To make this efficient we check
    // halfway down the depths. If still in the water column we try again
    // at 3/4 depth, then 7/8 and so on (binary search).
    private List<Double> getValidElevationsFromProfileValues(List<Float> profileValues) throws InvalidDimensionValueException, IOException {
        int index = profileValues.size() / 2;
        int prevIndex = 0;
        int shallowBound = 0;
        int deepBound = profileValues.size();
        boolean prevInWater = true;

        while(true) {
            Float shallowerDepth = profileValues.get(index - 1);
            Float deeperDepth = profileValues.get(index);
            boolean success = (shallowerDepth != null && deeperDepth == null);
            boolean inWater = (shallowerDepth != null && deeperDepth != null);

            if (success) // on the sea floor!
                break;
            else if (inWater) {
                if (prevInWater) { // before: in water, now: in water
                    shallowBound = index;
                    deepBound = deepBound;
                }
                else { // before: under sea floor, now: in water
                    shallowBound = index;
                    deepBound = prevIndex;
                }
                prevInWater = true;
            }
            else {
                if (!prevInWater) { // before: under sea floor, now: under sea floor
                    shallowBound = shallowBound;
                    deepBound = index;
                }
                else { // before: in water, now: under sea floor
                    shallowBound = prevIndex;
                    deepBound = index;
                }
                prevInWater = false;
            }
            prevIndex = index;
            index = ((shallowBound + deepBound) / 2);
        }

        List<Double> validElevations = this.allModelElevations.subList(0, index);
        return validElevations;
    }

    private List<Double> getValidElevationsAtThisPosition() throws InvalidDimensionValueException, IOException {
        List<Float> profileValues = layer.readProfile(dates.get(0), allModelElevations, obsPosition);
        return this.getValidElevationsFromProfileValues(profileValues);
    }
}
